1 /* 2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 3 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 4 Authors: Marcelo S. N. Mancini 5 6 Copyright Marcelo S. N. Mancini 2018 - 2021. 7 Distributed under the CC BY-4.0 License. 8 (See accompanying file LICENSE.txt or copy at 9 https://creativecommons.org/licenses/by/4.0/ 10 */ 11 module hip.assets.textureatlas; 12 public import hip.api.data.textureatlas; 13 import hip.assets.texture; 14 import hip.api.data.asset; 15 16 17 class HipTextureAtlas : HipAsset, IHipTextureAtlas 18 { 19 import hip.assets.image; 20 string atlasPath; 21 string[] texturePaths; 22 IHipTexture texture; 23 AtlasFrame[string] _frames; 24 25 ref inout(AtlasFrame[string]) frames() inout {return _frames;} 26 27 this() 28 { 29 super("TextureAtlas"); 30 _typeID = assetTypeID!HipTextureAtlas; 31 } 32 33 string getTexturePath () const 34 { 35 return texturePaths[0]; 36 } 37 38 39 bool loadTexture (in Image image) 40 { 41 import hip.assets.texture; 42 texture = new HipTexture(image); 43 if(!texture.hasSuccessfullyLoaded) 44 return false; 45 foreach(k, ref v; frames) 46 { 47 v.region = new HipTextureRegion(texture, 48 cast(uint)v.frame.x, cast(uint)v.frame.y, 49 cast(uint)(v.frame.x + v.frame.width), 50 cast(uint)(v.frame.y + v.frame.height) 51 ); 52 } 53 return true; 54 } 55 56 static HipTextureAtlas readJSON (const ubyte[] data, string atlasPath, string texturePath) 57 { 58 import hip.data.json; 59 import hip.assets.texture; 60 HipTextureAtlas ret = new HipTextureAtlas(); 61 ret.texturePaths~= texturePath; 62 ret.atlasPath = atlasPath; 63 64 import hip.console.log; 65 66 JSONValue json = parseJSON(cast(string)data); 67 if(json["frames"].type == JSONType.array) 68 { 69 foreach(f; json["frames"].array) 70 { 71 AtlasFrame a; 72 a.filename = f["filename"].str; 73 a.rotated = f["rotated"].boolean; 74 a.trimmed = f["trimmed"].boolean; 75 JSONValue frameRect = f["frame"].object; 76 a.frame = AtlasRect( 77 cast(uint)frameRect["x"].integer, 78 cast(uint)frameRect["y"].integer, 79 cast(uint)frameRect["w"].integer, 80 cast(uint)frameRect["h"].integer 81 ); 82 frameRect = f["spriteSourceSize"].object; 83 a.spriteSourceSize = AtlasRect( 84 cast(uint)frameRect["x"].integer, 85 cast(uint)frameRect["y"].integer, 86 cast(uint)frameRect["w"].integer, 87 cast(uint)frameRect["h"].integer 88 ); 89 frameRect = f["sourceSize"].object; 90 a.sourceSize = AtlasSize(cast(uint)frameRect["w"].integer, cast(uint)frameRect["h"].integer); 91 ret.frames[a.filename] = a; 92 } 93 } 94 else 95 { 96 JSONValue frames = json["frames"].object; 97 JSONValue meta = json["meta"].object; 98 foreach(string frameName, JSONValue f; frames) 99 { 100 AtlasFrame a; 101 a.filename = frameName; 102 a.rotated = f["rotated"].boolean; 103 a.trimmed = f["trimmed"].boolean; 104 JSONValue frameRect = f["frame"].object; 105 a.frame = AtlasRect( 106 cast(uint)frameRect["x"].integer, 107 cast(uint)frameRect["y"].integer, 108 cast(uint)frameRect["w"].integer, 109 cast(uint)frameRect["h"].integer 110 ); 111 frameRect = f["spriteSourceSize"].object; 112 a.spriteSourceSize = AtlasRect( 113 cast(uint)frameRect["x"].integer, 114 cast(uint)frameRect["y"].integer, 115 cast(uint)frameRect["w"].integer, 116 cast(uint)frameRect["h"].integer 117 ); 118 frameRect = f["sourceSize"].object; 119 a.sourceSize = AtlasSize(cast(uint)frameRect["w"].integer, cast(uint)frameRect["h"].integer); 120 ret.frames[frameName] = a; 121 } 122 } 123 124 return ret; 125 } 126 127 /** 128 * If no texturePath is given, it will try spritesheetPath<.png> 129 * I found a txt file that is parsed as: 130 * 131 `spriteName = x y width height` 132 */ 133 // static HipTextureAtlas readSpritesheet (string spritesheetPath, string texturePath = "") 134 // { 135 //TODO: FIX HIPFS 136 // import hip.filesystem.hipfs; 137 // string data; 138 // if(!HipFS.readText(spritesheetPath)) 139 // { 140 // import hip.error.handler; 141 // ErrorHandler.showWarningMessage("Could not find spritesheet from path ", spritesheetPath); 142 // return null; 143 // } 144 // import hip.util.path; 145 // if(texturePath == "") 146 // { 147 // texturePath = spritesheetPath.dup.extension(".png"); 148 // } 149 150 // return readSpritesheet(cast(ubyte[])data, spritesheetPath, texturePath); 151 // } 152 153 static HipTextureAtlas readSpritesheet (const ubyte[] data, string spritesheetPath, string texturePath) 154 { 155 import hip.util.string:splitRange, trim, isNumber; 156 import hip.assets.texture; 157 158 string toParse = cast(string)data; 159 160 HipTextureAtlas atlas = new HipTextureAtlas(); 161 atlas.atlasPath = spritesheetPath; 162 atlas.texturePaths~= texturePath; 163 164 165 foreach(line; splitRange(toParse, "\n")) 166 { 167 import hip.util.algorithm; 168 import hip.util.conv; 169 import hip.console.log; 170 line = line.trim(); 171 auto lineDataRange = splitRange(line, " "); 172 lineDataRange.popFront; 173 string frame = lineDataRange.front; 174 if(frame != "") 175 { 176 while(!lineDataRange.empty && !lineDataRange.front.isNumber) 177 { 178 lineDataRange.popFront; //Find a number 179 } 180 int x = void, y = void, width = void, height = void; 181 lineDataRange.map((string data) => to!int(data)).put(&x, &y, &width, &height); 182 AtlasFrame atlasFrame; 183 atlasFrame.spriteSourceSize = AtlasRect(x, y, width, height); 184 atlasFrame.frame = AtlasRect(x, y, width, height); 185 atlasFrame.sourceSize = AtlasSize(width, height); 186 atlasFrame.filename = frame; 187 atlas.frames[frame] = atlasFrame; 188 } 189 } 190 return atlas; 191 } 192 193 194 static HipTextureAtlas readFromMemory (const ubyte[] data, string atlasPath, string texturePath = ":IGNORE") 195 { 196 import hip.util.path; 197 switch(atlasPath.extension) 198 { 199 case "xml": 200 return HipTextureAtlas.readXML(data, atlasPath, texturePath); 201 case "atlas": 202 return HipTextureAtlas.readAtlas(data, atlasPath); 203 case "json": 204 return HipTextureAtlas.readJSON(data, atlasPath, texturePath == ":IGNORE" ? "" : texturePath); 205 default: 206 return HipTextureAtlas.readSpritesheet(data, atlasPath, texturePath == ":IGNORE" ? "" : texturePath); 207 } 208 } 209 210 /** 211 * Used for the following type of XML (Parsed without a real XML parser): 212 ```xml 213 <TextureAtlas imagePath="image.png"> 214 <SubTexture name="sub.png" x="0" y="0" width="0" height="0"/> 215 </TextureAtlas> 216 ``` 217 */ 218 static HipTextureAtlas readXML (const ubyte[] data, string atlasPath, string texturePath = ":IGNORE") 219 { 220 import hip.assets.texture; 221 import hip.util.string; 222 import hip.util.path; 223 import hip.util.conv; 224 string dataToParse = cast(string)data; 225 import hip.console.log; 226 //TODO: Fix .after (as it only executes startsWith and is returning null) 227 dataToParse = dataToParse.findAfter("imagePath="); 228 if(texturePath == ":IGNORE") 229 texturePath = atlasPath.dup.extension(".png"); 230 else 231 texturePath = dataToParse.between("\"", "\""); 232 HipTextureAtlas atlas = new HipTextureAtlas(); 233 atlas.texturePaths~= texturePath; 234 dataToParse = dataToParse.findAfter(">"); 235 236 foreach(line; dataToParse.splitRange("\n")) 237 { 238 line = line.trim(); 239 if(!line) 240 continue; 241 if(line.startsWith("</TextureAtlas>")) 242 break; 243 string name = (line = line.findAfter("name=")).between(`"`, `"`); 244 int x = (line = line.findAfter("x=")).between(`"`, `"`).to!int; 245 int y = (line = line.findAfter("y=")).between(`"`, `"`).to!int; 246 int width = (line = line.findAfter("width=")).between(`"`, `"`).to!int; 247 int height = (line = line.findAfter("height=")).between(`"`, `"`).to!int; 248 249 AtlasFrame frame; 250 frame.filename = name; 251 frame.frame = AtlasRect(x, y, width, height); 252 frame.spriteSourceSize = AtlasRect(x, y, width, height); 253 frame.sourceSize = AtlasSize(width, height); 254 atlas.frames[frame.filename] = frame; 255 } 256 return atlas; 257 } 258 259 260 static HipTextureAtlas readAtlas (const ubyte[] data, string atlasPath) 261 { 262 import hip.util.string : split, countUntil; 263 import hip.util.conv : to; 264 265 HipTextureAtlas ret = new HipTextureAtlas(); 266 ret.atlasPath = atlasPath; 267 string atlasFile = cast(string)data; 268 269 string[] lines = atlasFile.split("\n"); 270 int i = 0; 271 while(lines[i] == "") 272 i++; 273 string textureName = lines[i++]; 274 ret.texturePaths~= textureName; 275 string sizeText = lines[i++]; 276 string format = lines[i++]; 277 string filter = lines[i++]; 278 string repeat = lines[i++]; 279 280 const int offset = i; 281 282 for(; i < lines.length-offset; i+= 7) 283 { 284 AtlasFrame frame; 285 frame.trimmed = false; 286 frame.filename = lines[i]; 287 288 string rotate = lines[i+1]; 289 rotate = rotate[rotate.countUntil(":")+2 .. $]; 290 frame.rotated = to!bool(rotate); 291 292 string xy = lines[i+2]; 293 xy = xy[xy.countUntil(":")+2 .. $]; 294 295 ptrdiff_t commaIndex = xy.countUntil(','); 296 int x = to!int(xy[0..commaIndex]); 297 //To account space must increate 2 298 int y = to!int(xy[commaIndex+2..$]); 299 300 string size = lines[i+3]; 301 size = size[size.countUntil(":")+2 .. $]; 302 303 commaIndex = size.countUntil(','); 304 int sizeW = to!int(size[0..commaIndex]); 305 int sizeH = to!int(size[commaIndex+2..$]); 306 307 string orig = lines[i+4]; 308 orig = orig[orig.countUntil(":")+2 .. $]; 309 310 commaIndex = orig.countUntil(','); 311 int origX = to!int(orig[0..commaIndex]); 312 int origY = to!int(orig[commaIndex+2..$]); 313 314 string _offset = lines[i+5]; 315 _offset = _offset[_offset.countUntil(":")+2 .. $]; 316 317 commaIndex = _offset.countUntil(','); 318 int _offsetX = to!int(_offset[0..commaIndex]); 319 int _offsetY = to!int(_offset[commaIndex+2..$]); 320 321 string index = lines[i+6]; 322 index = index[index.countUntil(":")+2 .. $]; 323 324 frame.frame = AtlasRect(x, y, sizeW, sizeH); 325 frame.spriteSourceSize = AtlasRect(_offsetX, _offsetY, sizeW, sizeH); 326 frame.sourceSize = AtlasSize(sizeW, sizeH); 327 ret.frames[frame.filename] = frame; 328 } 329 return ret; 330 } 331 332 333 override void onFinishLoading(){} 334 override void onDispose(){} 335 override bool isReady() const {return texture !is null && frames.length > 0;} 336 337 338 alias frames this; 339 }